package oss

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

func ossHeaderJoin(ossHeader []string) string {
	if len(ossHeader) == 0 {
		return ""
	}

	sort.Strings(ossHeader)
	return strings.Join(ossHeader, "\n")
}
func makeOssHeader(header http.Header) string {
	if header == nil {
		return ""
	}

	var ossHeader []string
	for key, value := range header {
		key = strings.TrimSpace(strings.ToLower(key))
		if strings.HasPrefix(key, OssHeaderPrefix) {
			headerValue := strings.Join(value, ",")
			v := fmt.Sprintf("%s:%s", key, headerValue)
			ossHeader = append(ossHeader, v)
		}
	}

	return ossHeaderJoin(ossHeader)
}

func makeOssResource(path string, value url.Values) string {
	if value == nil {
		return path
	}

	var resources []string
	keys := make([]string, 0, len(value))
	for k := range value {
		keys = append(keys, k)
	}

	sort.Strings(keys)
	for _, k := range keys {
		vs := value[k]

		if len(vs) == 0 {
			resources = append(resources, url.QueryEscape(k))
		} else {
			for _, v := range vs {
				resources = append(resources, url.QueryEscape(k)+"="+url.QueryEscape(v))
			}
		}
	}

	return strings.Join(resources, "&")
}

func makeAuth(method string, path string, header http.Header, value url.Values) string {
	contentMd5 := header.Get("Content-Md5")
	contentType := header.Get("Content-Type")

	gmtTime := header.Get("Date")
	if gmtTime == "" {
		gmtTime = getGmtDateOnNow()
	}

	ossHeader := makeOssHeader(header)
	ossResource := makeOssResource(path, value)

	auth := []string{method, contentMd5, contentType, gmtTime}
	if ossHeader != "" {
		auth = append(auth, ossHeader)
	}

	auth = append(auth, ossResource)

	return strings.Join(auth, "\n")
}

func sign(accessKey string, method string, path string, header http.Header, value url.Values) string {
	auth := makeAuth(method, path, header, value)
	h := hmac.New(sha1.New, []byte(accessKey))
	h.Write([]byte(auth))
	return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func authOssHeader(accessKeyID string, authorization string) string {
	return fmt.Sprintf("OSS %s:%s", accessKeyID, authorization)
}

func (o OssAPI) newUrl(path string, value url.Values) string {
	var rawQuery string
	if len(value) > 0 {
		rawQuery = value.Encode()
	}

	scheme := "http"
	if o.isSecurity {
		scheme = "https"
	}

	host := o.host
	if o.bucket != "" {
		host = o.bucket + "." + host
	}

	u := url.URL{
		Scheme:   scheme,
		Host:     host,
		Path:     path,
		RawQuery: rawQuery,
	}

	return u.String()
}

func (o OssAPI) do(req *http.Request, header http.Header, response interface{}) (err error) {
	if header != nil {
		req.Header = header
	}
	resp, err := o.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	o.currentRequestId = resp.Header.Get("x-oss-request-id")
	o.currentObjectType = resp.Header.Get("x-oss-object-type")

	if response != nil {
		switch response.(type) {
		case io.Writer:
			buff := response.(io.Writer)
			io.Copy(buff, resp.Body)
		default:
			data, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				return err
			}
			if len(data) > 0 {
				err = xml.Unmarshal(data, response)
				if err != nil {
					return err
				}
			}
		}
	}

	return nil
}

func (o OssAPI) httpGet(path string, value url.Values, header http.Header, response interface{}) (err error) {
	authorization := sign(o.accessKeySecret, "GET", path, header, value)

	if header == nil {
		header = http.Header{}
	}

	header.Set("Authorization", authOssHeader(o.accessKeyID, authorization))

	u := o.newUrl(path, value)
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return err
	}
	return o.do(req, header, response)
}

func (o OssAPI) httpPost(path string, value url.Values, header http.Header, request interface{}, response interface{}) error {
	var body io.Reader
	var contentLength int
	switch request.(type) {
	case io.Reader:
		body = request.(io.Reader)
	default:
		data, err := xml.Marshal(request)
		if err != nil {
			return err
		}

		body = bytes.NewReader(data)
		contentLength = len(data)
	}

	authorization := sign(o.accessKeySecret, "POST", path, header, value)

	if header == nil {
		header = http.Header{}
	}

	header.Set("Authorization", authOssHeader(o.accessKeyID, authorization))
	if contentLength != 0 {
		header.Set("Content-Length", fmt.Sprintf("%d", contentLength))
	}

	u := o.newUrl(path, value)
	req, err := http.NewRequest("POST", u, body)
	if err != nil {
		return err
	}
	header.Set("Content-Type", DefaultContentType)
	return o.do(req, header, response)
}

func (o OssAPI) newMultipartRequest(path string, values url.Values, request interface{}) (*http.Request, error) {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	switch request.(type) {
	case map[string]string: // 文件名路径
		{
			paths := request.(map[string]string)
			for fieldName, path := range paths {
				fp, err := os.Open(path)
				if err != nil {
					return nil, err
				}
				defer fp.Close()

				part, err := writer.CreateFormFile(fieldName, filepath.Base(path))
				if err != nil {
					return nil, err
				}

				_, err = io.Copy(part, fp)
				if err != nil {
					return nil, err
				}
			}
		}
	case map[string]io.Reader:
		readers := request.(map[string]io.Reader)
		for fieldName, reader := range readers {
			part, err := writer.CreateFormFile(fieldName, fieldName)
			if err != nil {
				return nil, err
			}

			_, err = io.Copy(part, reader)
			if err != nil {
				return nil, err
			}
		}
	}

	for key, value := range values {
		for _, vv := range value {
			writer.WriteField(key, vv)
		}
	}

	err := writer.Close()
	if err != nil {
		return nil, err
	}

	u := o.newUrl(path, nil)

	return http.NewRequest("POST", u, body)
}

func (o OssAPI) httpPostMultipart(path string, value url.Values, header http.Header, request interface{}, response interface{}) error {
	req, err := o.newMultipartRequest(path, value, request)
	if err != nil {
		return err
	}

	header.Set("Content-Type", DefaultContentType)
	return o.do(req, header, response)
}

func (o OssAPI) httpPut(path string, value url.Values, header http.Header, request interface{}, response interface{}) error {
	var body io.Reader
	var contentLength int
	switch request.(type) {
	case io.Reader:
		body = request.(io.Reader)
	default:
		data, err := xml.Marshal(request)
		if err != nil {
			return err
		}

		body = bytes.NewReader(data)
		contentLength = len(data)
	}

	authorization := sign(o.accessKeySecret, "PUT", path, header, value)

	if header == nil {
		header = http.Header{}
	}

	header.Set("Authorization", authOssHeader(o.accessKeyID, authorization))
	if contentLength != 0 {
		header.Set("Content-Length", fmt.Sprintf("%d", contentLength))
	}

	u := o.newUrl(path, value)
	req, err := http.NewRequest("PUT", u, body)
	if err != nil {
		return err
	}
	header.Set("Content-Type", DefaultContentType)
	return o.do(req, header, response)
}

func (o OssAPI) httpDelete(path string, value url.Values, header http.Header, response interface{}) error {
	authorization := sign(o.accessKeySecret, "DELETE", path, header, value)

	if header == nil {
		header = http.Header{}
	}

	header.Add("Authorization", authOssHeader(o.accessKeyID, authorization))

	u := o.newUrl(path, value)
	req, err := http.NewRequest("DELETE", u, nil)
	if err != nil {
		return err
	}
	return o.do(req, header, response)
}

func (o OssAPI) httpHead(path string, header http.Header) error {
	req, err := http.NewRequest("HEAD", path, nil)
	if err != nil {
		return err
	}

	return o.do(req, nil, nil)
}
